Skip to content

feat: swap to using aw-mcpg#19

Open
jamesadevine wants to merge 15 commits intomainfrom
devinejames/remove-mcp-wrapper
Open

feat: swap to using aw-mcpg#19
jamesadevine wants to merge 15 commits intomainfrom
devinejames/remove-mcp-wrapper

Conversation

@jamesadevine
Copy link
Collaborator

No description provided.

jamesadevine and others added 4 commits March 8, 2026 23:06
Remove the custom MCP firewall proxy (src/mcp_firewall.rs), embedded tool
metadata (src/mcp_metadata.rs, mcp-metadata.json), and all firewall tests.

The copilot CLI no longer has built-in MCPs, so:
- Replace BUILTIN_MCPS constant with is_custom_mcp() helper
- Remove --disable-builtin-mcps, --disable-mcp-server, --mcp from copilot params
- Remove mcp-firewall CLI subcommand
- Simplify create wizard to MCP-level selection (no tool-level metadata)
- Update 1ES compiler to use is_custom_mcp() check
- Remove terminal_size dependency (only used by deleted MCP tool selector)

All MCPs are now handled through the MCP Gateway (MCPG) instead of
the custom firewall proxy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add infrastructure for the gh-aw-mcpg gateway integration:

- Add MCPG_VERSION and MCPG_IMAGE constants in common.rs
- Add {{ mcpg_version }} and {{ mcpg_image }} template markers
- Replace generate_firewall_config() with generate_mcpg_config() that
  produces MCPG-compatible JSON (mcpServers + gateway sections)
- SafeOutputs always included as HTTP backend via host.docker.internal
- Custom MCPs (with command:) become stdio servers in MCPG config
- Add host.docker.internal to CORE_ALLOWED_HOSTS for AWF container
  to reach host-side MCPG
- Add mcp-http subcommand: serves SafeOutputs over HTTP using rmcp's
  StreamableHttpService with axum, API key auth, and health endpoint
- Add axum and rmcp transport-streamable-http-server dependencies
- Remove terminal_size dependency (no longer used)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the legacy MCP firewall pipeline steps with MCPG architecture:

- Replace 'Prepare MCP firewall config' with 'Prepare MCPG config'
- Replace stdio MCP config (safeoutputs + mcp-firewall) with HTTP
  config pointing copilot to MCPG via host.docker.internal
- Add 'Start SafeOutputs HTTP server' step (background process on host)
- Add 'Start MCP Gateway (MCPG)' step (Docker container on host network)
- Add 'Stop MCPG and SafeOutputs' cleanup step (condition: always)
- Add --enable-host-access to AWF invocation for container-to-host access
- Pre-pull MCPG Docker image alongside AWF images
- Update step display names and comments

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add assertions for MCPG Docker image reference in compiled output
- Add assertions for host.docker.internal and --enable-host-access
- Add assertions verifying no legacy mcp-firewall references
- Add template structure checks for mcpg_config, mcpg_image, mcpg_version markers
- Verify template no longer contains mcp-firewall-config or MCP_FIREWALL_EOF
- Update fixture test to not require built-in MCP assertions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

github-actions bot commented Mar 8, 2026

🔍 Rust PR Review

Summary: Architecture replacement looks solid overall, but there are a few correctness bugs and security concerns worth addressing before merge.


Findings

🐛 Bugs / Logic Issues

  • safeoutputs omitted from MCPG config when no MCP servers are configured (src/compile/standalone.rs, generate_mcpg_config call site)

    generate_mcpg_config always inserts safeoutputs as an HTTP backend — but the call site only invokes it when !front_matter.mcp_servers.is_empty(). When a pipeline has zero mcp-servers: entries it falls through to the hardcoded {"mcpServers":{}} string, which omits safeoutputs entirely. That silently breaks safe-output tools for any agent that doesn't configure an extra MCP.

    Fix: call generate_mcpg_config unconditionally (or move safeoutputs insertion into the else branch):

    // Always generate — safeoutputs is always required
    let config = generate_mcpg_config(front_matter);
    let mcpg_config_json = serde_json::to_string_pretty(&config)
        .unwrap_or_else(|_| r#"{"mcpServers":{}}"#.to_string());
  • rt.block_on() called on a Tokio worker thread (src/mcp.rs, run_http)

    The factory closure passed to StreamableHttpService::new is almost certainly called from within the Tokio runtime when a new session is established. Calling Handle::current().block_on(…) on a Tokio worker thread panics with "Cannot start a runtime from within a runtime". SafeOutputs::new is an async fn, so the only safe patterns here are either making the closure async, or initializing SafeOutputs once outside the closure and cloning an Arc:

    // Pre-initialize and share via Arc
    let safe_outputs = Arc::new(SafeOutputs::new(&bounding, &output).await?);
    let mcp_service = StreamableHttpService::new(
        move || Ok((*safe_outputs).clone()),  // if SafeOutputs: Clone);
  • Readiness wait loop has no error path (templates/base.yml, "Start SafeOutputs HTTP server" / "Start MCP Gateway" steps)

    Both 30-iteration poll loops break on success but fall through silently on timeout. If either service fails to start, the pipeline continues and produces confusing failures later. A minimal guard:

    if ! curl -sf "(localhost/redacted) > /dev/null 2>&1; then
      echo "##vso[task.complete result=Failed]SafeOutputs did not become ready"
      exit 1
    fi

🔒 Security Concerns

  • host.docker.internal added to CORE_ALLOWED_HOSTS (src/allowed_hosts.rs)

    This hostname resolves to the Docker host from inside a container. Adding it to the core allow-list means every pipeline — including those that don't use MCPG at all — will have the AWF container able to reach any service running on the host. A safer approach is to add it only when MCPG is configured (similar to how MCP-specific hosts are added per-MCP in generate_allowed_domains).

  • Docker socket mounted into MCPG container (templates/base.yml)

    -v /var/run/docker.sock:/var/run/docker.sock \

    Mounting the Docker socket gives the MCPG container full control over the Docker daemon (spawn containers, access all volumes, escape network isolation). If MCPG doesn't strictly require socket access, remove it. If it does, this deserves an explicit security comment explaining why.

  • Weak fallback API key (src/mcp.rs, run_http)

    The generated fallback (format!("{:x}", SystemTime::now().as_nanos())) is ~48 bits of a predictable, time-based value. In practice the pipeline always passes --api-key $(openssl rand …), so the risk is low — but the Rust fallback will be hit in any dev/test invocation without --api-key. Consider using uuid::Uuid::new_v4() or any cryptographic RNG (e.g., fill a [u8; 32] from std::os::unix::fs::File on /dev/urandom) for the fallback path.

⚠️ Suggestions

  • Hardcoded gateway port 80 (src/compile/standalone.rs, McpgGatewayConfig)

    port: 80 is baked into the generated JSON. If MCPG ever changes its default port, this will silently produce a broken config. Consider surfacing this as a constant (MCPG_PORT) alongside MCPG_VERSION/MCPG_IMAGE, or deriving it from a template marker that can be replaced at compile time.

  • 1ES generate_mcp_configuration now includes all non-custom MCPs without checking whether a service connection exists (src/compile/onees.rs)

    The old code guarded on is_builtin_mcp. The new code passes any enabled, non-custom MCP through — including names like phantom that have no service connection and no builtin backing. The generated YAML will reference a non-existent serviceConnection, which fails at queue time. Worth adding a comment or a validation that warns when a non-custom MCP is used in 1ES mode.

✅ What Looks Good

  • Clean removal of mcp_firewall.rs and mcp_metadata.rs — the module boundaries are now much simpler.
  • McpgServerConfig / McpgConfig types are well-structured with appropriate skip_serializing_if guards.
  • The sed-based runtime substitution is safe: SAFE_OUTPUTS_API_KEY is generated with tr -d '/+=' leaving only [A-Za-z0-9], so no sed injection risk.
  • Auth middleware correctly skips /health and enforces Bearer token on all other routes.
  • Test coverage was updated proportionally with the deleted code.

Generated by Rust PR Reviewer for issue #19 ·

jamesadevine and others added 2 commits March 8, 2026 23:17
- Update architecture file tree (remove mcp_firewall.rs, mcp_metadata.rs, mcp-metadata.json)
- Replace {{ firewall_config }} marker docs with {{ mcpg_config }}
- Add {{ mcpg_version }} and {{ mcpg_image }} marker documentation
- Update {{ agency_params }} to remove MCP-related flags
- Replace mcp-firewall CLI docs with mcp-http subcommand
- Remove 'Built-in MCP Servers' section (no built-in MCPs exist)
- Simplify MCP Configuration to custom servers only
- Add host.docker.internal to allowed domains table
- Replace entire MCP Firewall section with MCP Gateway (MCPG) docs
- Update standalone target description for MCPG
- Update front matter example to remove built-in MCP references
- Add gh-aw-mcpg and gh-aw-firewall to References

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Always call generate_mcpg_config() regardless of mcp_servers being
  empty — safeoutputs must always be present in the MCPG config
- Pre-initialize SafeOutputs outside the StreamableHttpService factory
  closure to avoid block_on() panic on a Tokio worker thread
- Add failure guards to readiness wait loops in base.yml: both
  SafeOutputs and MCPG now fail the pipeline step explicitly if they
  don't become ready within 30 seconds

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

github-actions bot commented Mar 8, 2026

🔍 Rust PR Review

Summary: Good architectural refactoring — needs a couple of security fixes before merge.

The MCPG migration is clean and well-motivated. The removal of the custom MCP firewall layer in favour of the established gateway simplifies the stack significantly. The core logic is sound.

Findings

🔒 Security Concerns

  • src/mcp.rs:460-466 — Weak API key fallback: When --api-key is omitted, the generated key is format!("{:x}", SystemTime::now().as_nanos()) — a hex-encoded nanosecond timestamp (~40 bits, predictable and not cryptographically random). The pipeline always passes --api-key so this path is unlikely to be hit in practice, but anyone invoking ado-aw mcp-http directly would get a weak key. Use OsRng / getrandom or at minimum generate something non-deterministic:

    // Option: use the `rand` crate (already in dep tree via rmcp?)
    use rand::Rng;
    rand::thread_rng().gen::(u128)()

    Or surface a clearer error/warning that --api-key is required.

  • src/mcp.rs:529 — API key printed to log file: println!("SAFE_OUTPUTS_API_KEY={}", api_key) writes the secret to stdout, which the pipeline redirects to $(Agent.TempDirectory)/staging/logs/safeoutputs.log. If that log directory is ever published as a build artifact (or captured in diagnostics), the key is exposed. This println! is also redundant — the pipeline generates the key itself and passes it via --api-key, so the server doesn't need to echo it back. Remove or gate behind a debug log.

  • templates/base.yml:244 — Docker socket mount into MCPG: -v /var/run/docker.sock:/var/run/docker.sock gives the MCPG container full control over the Docker daemon on the host. This is a significant privilege escalation risk in shared pipeline agents. If MCPG only needs it to spawn stdio MCP servers as sibling processes, this should be justified in a comment and ideally scoped as tightly as possible (or handled through a daemon instead of socket pass-through).

🐛 Bugs / Logic Issues

  • src/compile/standalone.rs:166-167 — Silent serialization failure produces broken MCPG config:
    let mcpg_config_json = serde_json::to_string_pretty(&config)
        .unwrap_or_else(|_| r#"{"mcpServers":{}}"#.to_string());
    The fallback JSON is missing the gateway section, so MCPG would fail at startup with a cryptic error if this ever triggers. Since compile() returns Result, propagate with ?:
    let mcpg_config_json = serde_json::to_string_pretty(&config)
        .context("Failed to serialize MCPG config")?;

⚠️ Suggestions

  • src/compile/standalone.rs:~428args: Some(opts.args.clone()) unconditionally set: For custom MCPs with no args, the serialized JSON will include "args": []. Prefer if opts.args.is_empty() { None } else { Some(opts.args.clone()) } to match the clean handling already applied to env and tools — makes the generated config easier to read.

✅ What Looks Good

  • No leftover {{ firewall_config }} / mcp-firewall-config markers — the migration is complete and clean, verified by updated integration tests.
  • SafeOutputs is always injected as an HTTP backend regardless of mcp-servers: content — correct, since it's always required.
  • Auth middleware correctly bypasses the /health endpoint without requiring a token.
  • Cleanup steps (Stop MCPG and SafeOutputs) are guarded with condition: always() — good for failure scenarios.
  • 1ES path correctly warns and skips custom MCPs rather than silently generating broken config.
  • The generate_mcp_configuration fallback in onees.rs (mcp-{name}-service-connection) is a sensible convention for Enabled(true) MCPs.

Generated by Rust PR Reviewer for issue #19 ·

jamesadevine and others added 2 commits March 8, 2026 23:39
- Move host.docker.internal from CORE_ALLOWED_HOSTS to standalone
  compiler's generate_allowed_domains — only pipelines using MCPG
  need host access from the AWF container
- Replace weak time-based API key fallback with /dev/urandom (32
  bytes); time-based value retained only as last-resort if urandom
  is unavailable
- Remove SAFE_OUTPUTS_API_KEY println to avoid leaking the secret
  to log files; the pipeline already knows the key it generated
- Add security comment explaining why Docker socket mount is
  required for MCPG (spawns stdio MCP servers as sibling containers)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract MCPG_PORT constant alongside MCPG_VERSION/MCPG_IMAGE to
  avoid hardcoded port 80 in generated config
- Propagate MCPG config serialization error with ? instead of
  silently falling back to broken JSON missing the gateway section
- Omit empty args array from MCPG config for cleaner output
  (consistent with existing env/tools handling)
- Add warning in 1ES compiler when non-custom MCPs fall back to
  convention-based service connection names that may not exist

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

github-actions bot commented Mar 8, 2026

🔍 Rust PR Review

Summary: Good architectural direction — replacing the in-process MCP firewall with MCPG is the right call. Two security issues need addressing before merge; everything else is clean.


Findings

🐛 Bugs / Logic Issues

  • src/compile/standalone.rs:364safeoutputs name can be silently overwritten

    In generate_mcpg_config, the hard-coded SafeOutputs HTTP backend is inserted first, then the loop over front_matter.mcp_servers runs. If a user names their custom MCP server "safeoutputs":

    mcp_servers.insert("safeoutputs".to_string(), /* HTTP backend */);
    // ...loop overwrites it with user's stdio command
    mcp_servers.insert(name.clone(), McpgServerConfig { server_type: "stdio", command: Some("evil"), ... });

    The SafeOutputs HTTP backend gets replaced by a custom stdio command. The agent would then route all safeoutputs tool calls to the attacker-controlled process instead of the validated HTTP handler. Fix: guard the loop with if name == "safeoutputs" { log::warn!(...); continue; }.

🔒 Security Concerns

  • templates/base.yml — MCPG API key printed to pipeline logs before ADO masking applies

    In the "Start MCP Gateway (MCPG)" step, MCP_GATEWAY_API_KEY is a local bash variable. The ##vso[task.setvariable ...; issecret=true] command registers it as a secret only for subsequent steps — the current step's log output is not retroactively masked. The config (with the key substituted in) is then echoed:

    MCP_GATEWAY_API_KEY=$(openssl rand ...)
    echo "##vso[task.setvariable variable=MCP_GATEWAY_API_KEY;issecret=true]$MCP_GATEWAY_API_KEY"
    MCPG_CONFIG=$(... | sed "s|\${MCP_GATEWAY_API_KEY}|$MCP_GATEWAY_API_KEY|g")
    echo "$MCPG_CONFIG" | python3 -m json.tool   # ← key in plain text

    $(SAFE_OUTPUTS_API_KEY) is fine (it's an ADO secret from a prior step, so ADO masks it at runtime). But $MCP_GATEWAY_API_KEY is a shell-local variable here — ADO has no value to mask yet.

    Fix: either move the echo before the substitution (log the template, not the rendered config), or set the API key as a secret in a dedicated prior step so ADO has the value to mask.

⚠️ Suggestions

  • src/mcp.rs — entropy fallback is weak

    The /dev/urandom fallback uses a nanosecond SystemTime seed with a fixed multiplier. On Linux (the only supported pipeline target), /dev/urandom is always present, so this path is dead in production. Consider using rand::rngs::OsRng or the getrandom crate for a cleaner cross-platform solution that removes the dead code without relying on platform assumptions.

  • src/compile/allowed_hosts.rs comment vs. reality

    The comment says host.docker.internal is "added conditionally … when MCPG is configured." In practice it's added unconditionally in the standalone compiler (because MCPG is now always used). The comment is slightly misleading — worth updating to say "always added for standalone pipelines."

✅ What Looks Good

  • The MCPG config is always generated (SafeOutputs always present) — solid invariant; no conditional logic to forget.
  • Health-check polling loops with proper failure propagation (##vso[task.complete result=Failed]) before the agent step starts.
  • Cleanup step runs with condition: always() — both MCPG and SafeOutputs are stopped even on failure.
  • SAFE_OUTPUTS_PID capture + kill in the cleanup step is correct.
  • Test coverage for all the new MCPG config generation paths (always_includes_safeoutputs, custom_mcp, mcp_without_command_skipped, disabled_mcp_skipped, gateway_defaults).
  • Rename is_builtin_mcpis_custom_mcp with inverted logic is clean and the new semantics ("all MCPs are custom") are accurately reflected.

Generated by Rust PR Reviewer for issue #19 ·

- Guard reserved 'safeoutputs' name in generate_mcpg_config to prevent
  user-defined MCPs from overwriting the safe outputs HTTP backend
- Log MCPG config template before API key substitution to avoid leaking
  MCP_GATEWAY_API_KEY to pipeline logs (ADO secret masking only applies
  in subsequent steps, not the current step's output)
- Clarify allowed_hosts.rs comment: host.docker.internal is always added
  for standalone pipelines (not conditionally)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

github-actions bot commented Mar 8, 2026

🔍 Rust PR Review

Summary: Good architectural direction — the MCPG swap is clean and well-structured. A few reliability and security issues worth addressing before merge.

Findings

🐛 Bugs / Logic Issues

  • templates/base.yml — Hardcoded container name mcpg breaks retries

    docker run -i --rm --name mcpg --network host ...

    --rm only removes the container on clean exit. If the agent is interrupted (OOM kill, SIGKILL, network partition), the container may remain and the next pipeline retry will fail with docker: Error response from daemon: Conflict. The container name "/mcpg" is already in use. Add a pre-cleanup line before the docker run:

    docker rm -f mcpg 2>/dev/null || true
  • src/mcp.rs:run_http — Weak fallback entropy

    The last-resort API key path (when /dev/urandom is unavailable) derives entropy from only 8 bytes of SystemTime nanoseconds, then XOR-multiplies for the second half:

    buf[..16].copy_from_slice(&seed.to_le_bytes());
    buf[16..].copy_from_slice(&seed.wrapping_mul(0x517cc1b727220a95).to_le_bytes());

    The second 16 bytes are fully determined by the first 16 — this is a 64-bit key disguised as 256-bit. Since the binary only runs on Linux agents, /dev/urandom is always available in practice, but the fallback should either anyhow::bail!("cannot generate secure random key: ...") or use getrandom::getrandom. A silently weak key is worse than a clear error.

🔒 Security Concerns

  • src/mcp.rs:run_http — Non-constant-time API key comparison

    if auth_str == format!("Bearer {}", expected) {

    Short-circuit string equality leaks timing information proportional to how many leading bytes match. While the SafeOutputs HTTP server is only reachable via host.docker.internal (host-local), a compromised AWF container could use timing to brute-force the key incrementally. Swap for subtle::ConstantTimeEq or the constant_time_eq crate — both are already common in the Rust ecosystem.

  • templates/base.yml — Docker socket mount (intentional, but flagging)

    -v /var/run/docker.sock:/var/run/docker.sock

    The comment correctly documents this is intentional (MCPG spawns stdio MCP servers as sibling containers). Just ensuring reviewers are aware this grants the MCPG process full Docker daemon access on the host, meaning a compromised MCP server could escape isolation. Given the overall trust model this may be acceptable, but consider whether --network host + a restricted socket proxy (e.g. docker-socket-proxy) would provide adequate containment if stdio MCPs are untrusted.

⚠️ Suggestions

  • src/compile/standalone.rs:generate_mcpg_config — Duplicate warning arms

    The two else branches at the end of the MCP iteration loop produce identical log messages for two different conditions (an MCP with options but no command, vs. a bare McpConfig::Enabled(true) with no options). Collapsing them or adding distinguishing context ("MCP '{}' specified as boolean true — this format required built-in MCPs which no longer exist") would help users understand the migration path.

  • src/mcp.rs:run_httpSafeOutputs::new clone-based sharing

    Pre-initializing SafeOutputs and sharing via clone() works, but only if SafeOutputs's internal state (directories, file handles) is either immutable or Arc-wrapped. If SafeOutputs::clone() does a shallow copy of mutable state, concurrent sessions could race on writes to the staging directory. Worth a quick audit or a doc comment on the Clone impl confirming it's safe for concurrent use.

✅ What Looks Good

  • sed injection safety: tr -d '/+=' correctly strips all sed metacharacters from both SAFE_OUTPUTS_API_KEY and MCP_GATEWAY_API_KEY, making the runtime substitution safe.
  • "safeoutputs" name reservation with a clear warning log is a good defensive check.
  • condition: always() on both cleanup steps ensures MCPG and SafeOutputs are torn down even after agent failure.
  • context() added to MCPG config serialization (replacing the old unwrap_or_else) is a genuine improvement.
  • Test suite is cleanly updated to match the new architecture — no orphaned test helpers or references to removed types.
  • The 1ES target correctly warns (rather than silently dropping) when MCPs fall back to the naming convention for service connections.

Generated by Rust PR Reviewer for issue #19 ·

jamesadevine and others added 2 commits March 10, 2026 22:04
- Use subtle::ConstantTimeEq for API key comparison to prevent
  timing side-channel attacks from a compromised AWF container
- Fail loudly (panic) when /dev/urandom is unavailable instead of
  silently generating a weak time-based key
- Add doc comment on SafeOutputs Clone confirming concurrent safety:
  only contains immutable PathBuf fields, file I/O opens fresh

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Pre-cleanup stale mcpg container before docker run to prevent
  retry failures when a previous run was interrupted (OOM/SIGKILL
  leaves container behind despite --rm)
- Differentiate duplicate warning messages for MCPs without commands:
  options-but-no-command vs boolean-enabled (helps users understand
  migration path from removed built-in MCPs)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

🔍 Rust PR Review

Summary: Architecture swap looks solid overall — clean removal of the MCP firewall in favour of MCPG. A few issues worth addressing before merge.

Findings

🐛 Bugs / Logic Issues

  • src/mcp.rs ~line 488panic! inside a function whose signature is pub async fn run_http(...) -> Result<()>. In an async context panics aren't caught by the normal ? chain; they'll abort the Tokio task and produce a hard crash rather than a clean error message printed to the user. Replace with anyhow::bail! (or return Err(anyhow::anyhow!(...))) so the caller's error handling propagates normally and the process exits via the standard ? chain in main.

    // current
    panic!(
        "Cannot generate secure API key: /dev/urandom unavailable ({}). \
        Pass --api-key explicitly.",
        e
    );
    // suggested
    anyhow::bail!(
        "Cannot generate secure API key: /dev/urandom unavailable ({}). \
        Pass --api-key explicitly.",
        e
    );
  • templates/base.yml MCPG startup — The MCPG container is launched with --network host but the generated mcpg-config.json references host.docker.internal as the SafeOutputs URL. On Linux, host.docker.internal is not automatically injected into containers that use --network host (unlike Docker Desktop on macOS/Windows). In host-network mode the container shares the host's network namespace, so localhost or 127.0.0.1 works directly and is more reliable. Either switch the generated URL to localhost for MCPG's config, or add --add-host=host.docker.internal:host-gateway to the docker run command.

🔒 Security Concerns

  • src/mcp.rs ~line 536SafeOutputs HTTP server binds to 0.0.0.0 (all interfaces). Since MCPG runs in --network host mode, it can reach the server via localhost, so binding to 127.0.0.1 is sufficient and reduces exposure on shared build agents where other processes or network listeners are present.

    // current
    let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
    // suggested
    let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
  • templates/base.yml Docker socket mount-v /var/run/docker.sock:/var/run/docker.sock is documented in an inline comment as intentional (MCPG needs it to spawn stdio MCPs as sibling containers), and the pipeline environment is trusted/network-isolated, so this is acceptable. Just ensuring the design decision is explicit in the review record.

⚠️ Suggestions

  • src/compile/standalone.rs generate_allowed_domainshost.docker.internal is now explicitly excluded from CORE_ALLOWED_HOSTS (good — the comment explains why), but there's no corresponding entry in mcp_required_hosts either. The standalone path adds it directly in generate_allowed_domains. This is fine, but it means 1ES pipelines (which reuse the allowed-hosts logic) won't include it. If 1ES ever adopts MCPG this will silently miss the entry. A brief // standalone-only comment at the insertion site would help future readers.

  • src/compile/onees.rs generate_mcp_configuration — The old code filtered by is_builtin_mcp(name) before generating service-connection entries; the new code now emits a log::warn and skips any MCP with a command, but continues to add all non-command MCPs (regardless of whether they're actually intended for 1ES). Since there are no longer built-ins to whitelist against, a user who accidentally omits command: in their front matter for a standalone-style MCP will silently get a fabricated mcp-(name)-service-connection reference in the 1ES output (with only a log-level warning). Consider emitting the warning to stderr or returning a compile-time diagnostic instead.

✅ What Looks Good

  • subtle::ConstantTimeEq for API-key verification — correct and well-commented.
  • safeoutputs name reservation in generate_mcpg_config with a clear warning.
  • Struct-level #[serde(rename_all = "camelCase")] on McpgConfig/McpgGatewayConfig produces the correct mcpServers/apiKey JSON field names.
  • Pre-substitution config printed to logs (never post-substitution) — correctly avoids leaking the MCP_GATEWAY_API_KEY local bash variable before ADO masking kicks in.
  • skip_serializing_if = "Option::is_none" annotations keep the generated JSON clean.
  • docker rm -f mcpg ... || true guard before startup — good defensive practice.
  • Test coverage updated to match the new MCPG model; old firewall-specific tests removed cleanly.

Generated by Rust PR Reviewer for issue #19 ·

jamesadevine and others added 2 commits March 10, 2026 22:20
- Replace panic! with anyhow bail via .context() for /dev/urandom
  failure — panics in async contexts crash the Tokio task instead of
  propagating cleanly through the Result chain
- Bind to 127.0.0.1 instead of 0.0.0.0 — MCPG runs with --network
  host so localhost is sufficient, reducing exposure on shared agents

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SafeOutputs URL in MCPG config changed from host.docker.internal
  to localhost — MCPG runs with --network host on Linux where
  host.docker.internal is not auto-injected in host network mode
- Gateway domain remains host.docker.internal (used by AWF container
  which runs in bridge network mode with --enable-host-access)
- Add docker rm -f pre-cleanup before MCPG container start to handle
  interrupted retry scenarios
- Differentiate MCP warning messages for options-without-command vs
  boolean-enabled formats

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

🔍 Rust PR Review

Summary: Overall a well-structured refactor — removes a significant amount of complexity by delegating MCP routing to MCPG. A few issues worth addressing before merge.

Findings

🐛 Bugs / Logic Issues

  • src/mcp.rs (auth middleware)Response::builder().status(401).body(...).unwrap() is in a hot path (every unauthorized request). While this specific builder call can't realistically fail, it violates the project's own explicit convention: "No silent unwrap() or expect() on user-facing paths." Replace with axum::http::StatusCode::UNAUTHORIZED.into_response() (requires axum::response::IntoResponse).

  • src/compile/standalone.rs (warning message typo)"Boolean-enabled MCPs required built-in MCPs which no longer exist." — "required" should be "require" (present tense). Minor but shows up in user-visible log output.

🔒 Security Concerns

  • Agent → MCPG unauthenticated (templates/base.yml, the mcp-config.json step): The copilot agent's MCP config connects to MCPG without any Authorization header:

    {
      "mcpServers": {
        "mcpg": {
          "type": "http",
          "url": "(host.docker.internal/redacted)
        }
      }
    }

    The MCPG config has a gateway.apiKey (${MCP_GATEWAY_API_KEY}), but this key is never passed back to the agent for use in client requests. If MCPG enforces client auth via this key, the agent will fail to connect. If MCPG accepts unauthenticated clients (relying purely on network isolation for trust), that's fine but should be explicitly documented. Please confirm which applies.

  • Docker socket mount (templates/base.yml): MCPG gets -v /var/run/docker.sock:/var/run/docker.sock. The comment says this is needed because "MCPG spawns stdio-based MCP servers as sibling containers." However, the custom MCPs in this PR are configured as type: "stdio" with a host command: — not as containers. Is the socket really required? If MCPG uses it to spawn custom MCPs as sibling Docker containers, those processes effectively escape the AWF network-isolation sandbox (they'd run on the host network, not inside AWF). Recommend verifying whether MCPG needs the socket for this use case or if it can spawn stdio processes directly.

⚠️ Suggestions

  • src/compile/standalone.rs (safeoutputs name reservation): The reservation check is case-sensitive (if name == "safeoutputs"). A user could name an MCP "SafeOutputs" or "SAFEOUTPUTS", which would collide if MCPG is case-insensitive about server keys. Consider if name.eq_ignore_ascii_case("safeoutputs").

  • src/mcp.rs (API key generation fallback): The /dev/urandom path is Linux-only. The comment says this is a dev/test fallback since production always passes --api-key, but on Windows this will error with a confusing OS error. Since the project builds for Linux agents anyway this is low priority, but getrandom crate or rand would be more portable.

✅ What Looks Good

  • Correct use of subtle::ConstantTimeEq for timing-safe API key comparison in the auth middleware — this is the right approach to prevent timing side-channel attacks from an AWF-compromised container.
  • serde_json::to_string_pretty(&config).context("...") replacing the old silent unwrap_or_else — better error visibility.
  • Single-quoted heredoc delimiter ('MCPG_CONFIG_EOF') correctly preserves ${SAFE_OUTPUTS_PORT} etc. as literal placeholders for runtime sed substitution.
  • safeoutputs reservation check with a clear log warning prevents silent config clobbering.
  • Health check polling with 30s timeout and fail-fast on both SafeOutputs and MCPG startup — prevents silent hangs.
  • Test coverage for the new generate_mcpg_config function is thorough and correctly updated.

Generated by Rust PR Reviewer for issue #19 ·

jamesadevine and others added 2 commits March 10, 2026 22:50
MCPG enforces authentication on incoming client requests. The agent's
mcp-config.json now includes an Authorization header with the gateway
API key. To support this, the MCP_GATEWAY_API_KEY is generated in the
'Prepare MCPG config' step (as an ADO secret variable) instead of the
'Start MCP Gateway' step, making it available when writing both the
MCPG server config and the agent's client config.

Also updates MCPG startup step to reference the ADO variable via
$(MCP_GATEWAY_API_KEY) instead of the shell-local variable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace Response::builder().unwrap() with IntoResponse tuple for
  the 401 response in auth middleware (avoids unwrap on user path)
- Case-insensitive safeoutputs name reservation check to prevent
  collision via 'SafeOutputs' or 'SAFEOUTPUTS' variants
- Fix typo: 'required' → 'require' in user-visible warning message

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link

🔍 Rust PR Review

Summary: Solid architectural migration from the custom MCP firewall to MCPG. A few issues worth addressing before merge.

Findings

🐛 Bugs / Logic Issues

  • AGENTS.md documents the wrong SafeOutputs URL — the new {{ mcpg_config }} section says the SafeOutputs HTTP backend URL points to host.docker.internal ("url": "(host.docker.internal/redacted) in the example), but the code in standalone.rsgenerates(localhost/redacted) The code is correct (the comment in generate_mcpg_config explains why: MCPG uses --network host so Docker DNS isn't available), but the documentation added in this PR is wrong and will mislead operators debugging connectivity issues.

🔒 Security Concerns

  • templates/base.yml: Docker socket mount is very broad — MCPG is started with -v /var/run/docker.sock:/var/run/docker.sock. The comment acknowledges this grants significant host access and cites "MCPG spawns stdio-based MCP servers as sibling containers" as justification. Worth confirming this is actually how MCPG runs stdio MCPs (i.e., it requires Docker to spawn them) rather than running them as child processes inside its own container. If MCPG can run stdio MCPs in-process, the socket mount is unnecessary and amounts to handing effective root on the host to any custom MCP server the pipeline configures.

⚠️ Suggestions

  • templates/base.yml: sed substitution of secret ADO variables — the "Start MCP Gateway" step substitutes $(SAFE_OUTPUTS_API_KEY) and $(MCP_GATEWAY_API_KEY) via sed. This works correctly because both keys are generated with openssl rand -base64 45 | tr -d '/+=' (alphanumeric only, no | or other sed metacharacters). This dependency is invisible — worth adding a comment like # Keys are alphanumeric-only (generated above) so sed | delimiter is safe.

  • src/compile/onees.rs: warning-only for missing command in 1ES — when McpConfig::Enabled(true) (the boolean form) is used in a 1ES pipeline, opts is None, is_custom_mcp returns false, and the code falls through to generate a mcp-{name}-service-connection reference with just a log warning. The pipeline will compile successfully but fail at runtime if that service connection doesn't exist. Consider making this a compile-time error (anyhow::bail!) or at minimum making the warning message clearer that this will likely cause a runtime failure.

  • src/mcp.rs: /dev/urandom read vs OsRng — the fallback key generation uses std::fs::File::open("/dev/urandom") directly. Since the project already has rand or similar in the dependency tree (worth checking), using rand::rngs::OsRng would be more idiomatic Rust and cross-platform. Not a bug since these pipelines only run on Linux agents, but worth noting.

✅ What Looks Good

  • Error propagation fixed: serde_json::to_string_pretty now propagates errors via .context() instead of the old unwrap_or_else(|_| fallback) — good improvement.
  • safeoutputs name reservation: Case-insensitive check prevents user-defined MCPs from silently overwriting the built-in backend.
  • HTTP server bound to 127.0.0.1: SafeOutputs only listens on loopback — correct.
  • condition: always() on cleanup: MCPG and SafeOutputs are always stopped even on agent task failure.
  • MCPG config always generated: Even with no mcp-servers in front matter, SafeOutputs is always included as an HTTP backend — correct since the agent always needs it.
  • Constant-time auth comparison: Using subtle::ConstantTimeEq in the HTTP middleware is the right call.

Generated by Rust PR Reviewer for issue #19 ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant